#include "CUtil.h"
#include <filesystem.hpp>
#include <json.hpp>
#include <stb_include.h>
#include "CLog.h"
#include "CString.h"
#include "CElapsedTimer.h"
#if _WIN32
	#include <gl/GL.h>
	#define GL_SAMPLE_BUFFERS 0x80A8
	#define GL_SAMPLES 0x80A9
	#define GL_MAX_SAMPLES 0x8D57
#else
	#include <GLES3/gl3.h>
#endif

/**
 * @brief There is essentially no difference between `char` and `unsigned char` in terms of which characters they can represent.
 *        Both can store all characters in the standard ASCII character set (since the ASCII character set only uses code points from 0 to 127).
 *        The distinction mainly arises from how each type interprets the underlying numerical values.
 */
void CUtil::readFileAsString(std::string& out, const std::string& filePath) {
	CByteArray bytes;
	readFileAsBytes(bytes, filePath);
	out = std::string(bytes.begin(), bytes.end());
}

void CUtil::readFileAsBytes(CByteArray& out, const std::string& filePath) {
	if (!fileExists(filePath))
		throw std::runtime_error("file not exists: " + filePath);
	std::ifstream file(filePath, std::ios::in | std::ios::binary);
	if (!file)
		throw std::runtime_error("open file fail: " + filePath);
	file.seekg(0, std::ios::end);
	std::streamsize size = file.tellg();
	file.seekg(0, std::ios::beg);// move to file's start position
	out.resize(size);
	if (!file.read(reinterpret_cast<char*>(out.data()), size))
		throw std::runtime_error("read file fail: " + filePath);
	file.close();
}

bool CUtil::saveMsgToFile(const std::string& msg, const std::string& path) {
	std::ofstream outFile(path);
	if (!outFile) {
		LogE << "Unable to open file for writing: " << path;
		return false;
	}
	outFile << msg;
	if (!outFile) {
		LogE << "Failed to write message to file: " << path;
		outFile.close();
		return false;
	}
	outFile.close();
	return true;
}

/**
 *@details https://github.com/nothings/stb/blob/master/stb_include.h
 * Processes the input string, replacing #include directives with file contents and #inject with the given inject string.
 *
 * @param str                The input source string to process. May contain #include and #inject directives.
 * @param path_to_includes   Directory path to search for files referenced by #include "filename" directives.
 * @param inject             String to insert in place of any #inject directive found in the input.
 * @param filename           Filename to use in #line directives for error reporting and debugging.
 * @param error              Output buffer (size 256) where error messages will be written if processing fails.
 *
 * @return                   A newly allocated processed string with includes and injections. Caller must free the result.
 */
void CUtil::replaceIncludes(std::string& glslCode, const std::string& path_to_includes, char *inject, char *filename_for_line_directive, char error[256]) {
	if (glslCode.empty())
		return;
	char* result = stb_include_string(
		const_cast<char*>(glslCode.c_str()),
		inject,
		const_cast<char*>(path_to_includes.c_str()),
		filename_for_line_directive,
		error
	);
	
	if (!result) {
		LogE << "Failed to process GLSL includes! glslCode = " << glslCode;
		return;
	}
	
	glslCode = result;
	free(result);// must! because of 'malloc'
}

void CUtil::delay(const std::function<void()>& func, int delayMs) {
	std::thread([=]() {
		msleep(delayMs);
		func();
	}).detach();
}

void CUtil::loop(const std::function<void()>& func, int intervalMs) {
	std::thread([=]() {
		while (true) {
			func();
			msleep(intervalMs);
		}
	}).detach();
}

long long CUtil::currentTimeMillis() {
	return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}

std::string CUtil::currentTime() {
	auto now = std::chrono::system_clock::now();
	std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
	std::tm timeStruct = {};
	#ifdef _WIN32
		localtime_s(&timeStruct, &now_time_t);// Windows
	#else
		localtime_r(&now_time_t, &timeStruct);// POSIX
	#endif
	auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
	std::ostringstream oss;
	oss << std::put_time(&timeStruct, "%H:%M:%S");
	oss << ":" << std::setfill('0') << std::setw(3) << ms.count();
	return oss.str();
}

bool CUtil::fileExists(const std::string& path) {
	return ghc::filesystem::exists(path);
}

bool CUtil::mkdirIfNotExists(const std::string& path) {
	namespace fs = ghc::filesystem;
	fs::path dirPath(path);
	if (!dirPath.extension().empty())// file (if path has a file extension, consider as a file not dir)
		dirPath = dirPath.parent_path();
	if (!fs::exists(dirPath))
		return ghc::filesystem::create_directories(dirPath);
	return false;
}

std::string CUtil::getParentPath(const std::string& fullPath) {
	namespace fs = ghc::filesystem;
	fs::path path(fullPath);
	return path.parent_path().string();
}

/**
 * @example fromPath = "c:/user/mySrcFolder"，toPath = "d:/dir/myDestFolder"
 * copy whole folder 'mySrcFolder' to 'myDestFolder', finally => d:/dir/myDestFolder/mySrcFolder
 */
bool CUtil::copy(const std::string& fromPath, const std::string& toPath) {
	namespace fs = ghc::filesystem;
	try {
		if (!fs::exists(fromPath) || !fs::is_directory(fromPath)) {
			LogE << "Source path does not exist or is not a directory";
			return false;
		}
		if (!fs::exists(toPath))
			fs::create_directories(toPath);// if dest path is not exists, create
		fs::path finalDestPath = fs::path(toPath) / fs::path(fromPath).filename();
		if (fs::exists(finalDestPath))
			fs::remove_all(finalDestPath);// clear contents that exists already
		fs::copy(fs::path(fromPath), finalDestPath, fs::copy_options::recursive);
		return true;
	} catch (const fs::filesystem_error& e) {
		LogE << "Filesystem error: " << e.what();
	} catch (const std::exception& e) {
		LogE << "General error: " << e.what();
	}
	return false;
}

/**
* @param filterExtension like: .txt, .json, ...
*/
bool CUtil::filesOfDir(std::vector<std::string>& outFileNames, const std::string& dirPath, const std::string& filterExtension, bool asc) {
	namespace fs = ghc::filesystem;
	try {
		LogI << ">>> parsing large amount of files as test datas int a directory at new thread...... (" << dirPath << ")";
		CElapsedTimer timer;
		bool noFilter = filterExtension.empty();
		for (auto& entry : fs::directory_iterator(dirPath)) {
			if (noFilter)
				outFileNames.push_back(entry.path().filename().string());
			else {
				if (fs::is_regular_file(entry.status())) {// this logic consume too much time(some weak hardware may spend more than 10 seconds)
					std::string ext = entry.path().extension().string();
					if (ext == filterExtension)
						outFileNames.push_back(entry.path().filename().string());
				}
			}
		}
		LogI << "<<< parsing large amount of files spend: " << timer.elapsed() << "ms";
		timer.reset();
		std::sort(outFileNames.begin(), outFileNames.end(), [asc](const std::string& a, const std::string& b) {
			if (asc) return a.length() < b.length() || (a.length() == b.length() && a < b);
			else return a.length() > b.length() || (a.length() == b.length() && a > b);
		});
		return true;
	} catch (const fs::filesystem_error& e) {
		LogE << "filesystem error: " << e.what();
	}
	return false;
}

void CUtil::saveMapToLocalJson(const std::map<std::string, std::any>& KVs, const std::string& localPath) {
	nlohmann::json json;
	for (auto& kv : KVs) {
		auto value = kv.second;
		auto& type = value.type();
		try {
			if (type == typeid(bool)) json[kv.first] = std::any_cast<bool>(value);
			else if (type == typeid(int)) json[kv.first] = std::any_cast<int>(value);
			else if (type == typeid(double)) json[kv.first] = std::any_cast<double>(value);
			else if (type == typeid(float)) json[kv.first] = std::any_cast<float>(value);
			else if (type == typeid(const char*)) json[kv.first] = std::any_cast<const char*>(value);
			else if (type == typeid(std::string)) json[kv.first] = std::any_cast<std::string>(value);
		} catch (const std::bad_any_cast& e) {
			LogE << "failed to cast: " << e.what();
			std::abort();
		}
	}
	std::ofstream outFile(localPath);
	if (!outFile.is_open()) {
		LogE << "Failed to open file for writing: " << localPath;
		std::abort();
	}
	outFile << json.dump();// json.dump(4); use 4 space to format
	outFile.close();
}

void CUtil::getMapFromLocalJson(std::map<std::string, std::any>& outKVs, const std::string& localPath) {
	std::ifstream inFile(localPath);
	if (!inFile.is_open()) {
		LogE << "Failed to open file for reading: " << localPath;
		std::abort();
	}
	nlohmann::json jLoaded;
	try {
		inFile >> jLoaded;
	} catch (const nlohmann::json::parse_error& e) {
		LogE << "JSON parse error: " << e.what();
		std::abort();
	}
	inFile.close();
	for (auto& kv : outKVs) {
		auto& type = kv.second.type();
		try {
			if (type == typeid(bool)) kv.second = jLoaded.at(kv.first).get<bool>();
			else if (type == typeid(int)) kv.second = jLoaded.at(kv.first).get<int>();
			else if (type == typeid(double)) kv.second = jLoaded.at(kv.first).get<double>();
			else if (type == typeid(float)) kv.second = jLoaded.at(kv.first).get<float>();
			else if (type == typeid(const char*)) kv.second = jLoaded.at(kv.first).get<std::string>();
			else if (type == typeid(std::string)) kv.second = jLoaded.at(kv.first).get<std::string>();
		} catch (const std::bad_any_cast& e) {
			LogE << "failed to cast: " << e.what();
			std::abort();
		}
	}
}

void CUtil::printCompilerInfo() {
	std::string compiler_name;
	std::string compiler_version;
	std::string cpp_standard;

	// check compiler type
	#if defined(__clang__)
		compiler_name = "Clang";
		compiler_version = __clang_version__;
	#elif defined(__GNUC__)
		compiler_name = "GCC";
		compiler_version = std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__);
	#elif defined(_MSC_VER)
		// MSVC specified
		compiler_name = "MSVC";
		compiler_version = std::to_string(_MSC_VER);
		
		// MSVC's C++ standard
		#if _MSVC_LANG >= 202302L
			cpp_standard = "C++23";
		#elif _MSVC_LANG == 202002L
			cpp_standard = "C++20";
		#elif _MSVC_LANG == 201703L
			cpp_standard = "C++17";
		#elif _MSVC_LANG == 201402L
			cpp_standard = "C++14";
		#elif _MSVC_LANG == 201103L
			cpp_standard = "C++11";
		#else
			cpp_standard = "C++98 or older";
		#endif
	#else
		compiler_name = "Unknown Compiler";
		compiler_version = "Unknown";
	#endif

	// other than MSVC comipler C++ standard
	#if !defined(_MSC_VER)
		#if __cplusplus >= 202302L
			cpp_standard = "C++23";
		#elif __cplusplus == 202002L
			cpp_standard = "C++20";
		#elif __cplusplus == 201703L
			cpp_standard = "C++17";
		#elif __cplusplus == 201402L
			cpp_standard = "C++14";
		#elif __cplusplus == 201103L
			cpp_standard = "C++11";
		#elif __cplusplus == 199711L
			cpp_standard = "C++98";
		#else
			cpp_standard = "Unknown (possibly preview of a newer standard)";
		#endif
	#endif

	LogI << "Compiler: " << compiler_name << ", Version: " << compiler_version << ", C++ Standard: " << cpp_standard;
}

void CUtil::checkGLMultisamplingStatus() {
	if(glClear == nullptr) {
		LogE << "gl function pointer not loaded correctly!!!!!!";
		return;
	}
	GLint maxSamples = -1, sampleBuffers = -1, sampleCount = -1;
	glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
	glGetIntegerv(GL_SAMPLES, &sampleCount);
	glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);

	auto info = CString("Has enabled sample buffers(at least one): %1, Has enabled sample count(at least two): %2; Sys-supported maxSamples: %3").arg(sampleBuffers).arg(sampleCount).arg(maxSamples);
	if (maxSamples <= 1 || sampleBuffers <= 0 || sampleCount <= 1)
		LogW << "Multi-Sampling may not-enabled by surface or GL_MULTISAMPLE not supported! => " << info;
	else
		LogI << "GL_MULTISAMPLE is supported and enabled! => " << info;
}